返回 引气・Java 气海初拓

05重走我的Java Day05:面向对象——从“理解概念”到“设计思维”的蜕变

博主
大约 18 分钟

重走我的Java Day05:面向对象——从“理解概念”到“设计思维”的蜕变

面向对象不是一种语法,而是一种思维方式。我曾以为我懂了,直到我的第一个面向对象项目变成了“面向灾难”。

开篇:那个让我重构三次的“学生管理系统”

二年前,我接到了第一个正式项目:为学校的选修课系统开发一个简单的学生管理模块。需求很明确:学生可以选课,老师可以打分,管理员可以查看统计。

我信心满满地开始编码,第一版是这样写的:

image-20260201200435021

java

// 版本1:面向过程思维
public class StudentManager {
    public static void main(String[] args) {
        // 用数组存储所有数据
        String[] studentNames = new String[100];
        int[] studentAges = new int[100];
        String[] courseNames = new String[50];
        int[][] scores = new int[100][50];  // 学生×课程的成绩矩阵
        
        // 操作数据的函数
        addStudent(studentNames, studentAges, "张三", 20);
        addCourse(courseNames, "Java编程");
        selectCourse(studentNames, courseNames, "张三", "Java编程");
        setScore(scores, studentNames, courseNames, "张三", "Java编程", 85);
        
        // 随着功能增加,这个文件膨胀到了2000行...
    }
}

代码评审时,我的导师看了五分钟,然后说:“这不是面向对象,这是用Java写的C语言。” 他要求我全部重写

第二次评审,他说:“你现在有了对象,但没有对象思维。”

第三次,我才真正明白了什么是面向对象。

一、类与对象:从“数据容器”到“责任主体”的认知升级

1. 类的本质:不是“数据+方法”,而是“契约+责任”

教科书说“类是对象的模板”,这个定义太浅。我现在的理解是:类是现实世界某个概念的抽象,它定义了该概念的属性(状态)和行为(责任),以及与外部交互的契约

对比我最初的设计和最终的设计:

image-20260201200621823

java

// ❌ 最初:把类当作数据容器
public class Student {
    // 把所有可能用到的数据都放进来
    private String name;
    private int age;
    private String studentId;
    private String className;
    private String department;
    private String phone;
    private String email;
    private String address;
    private List<Course> selectedCourses;
    private Map<Course, Double> scores;
    private Date enrollmentDate;
    private boolean isGraduated;
    // ... 还有20个字段
    
    // 把所有相关操作都放进来
    public void selectCourse(Course course) { ... }
    public void dropCourse(Course course) { ... }
    public void updateScore(Course course, double score) { ... }
    public void printReportCard() { ... }
    public void calculateGPA() { ... }
    public void payTuition() { ... }
    public void applyForScholarship() { ... }
    // ... 还有15个方法
}

// ✅ 最终:单一职责,清晰边界
public class Student {
    private StudentInfo info;           // 基本信息
    private AcademicRecord record;      // 学业记录
    private ContactInfo contact;        // 联系信息
    
    // 只负责与学生身份直接相关的核心行为
    public void selectCourse(Course course) { ... }
    public void dropCourse(Course course) { ... }
}

// 其他职责拆分到专门的类
public class StudentInfo { ... }        // 只负责存储基本信息
public class AcademicRecord { ... }     // 只负责成绩、课程记录
public class FinanceService { ... }     // 处理缴费、奖学金
public class ReportGenerator { ... }    // 生成报告单

关键洞察:一个类不应该因为它“可能被用到”就包含某个字段或方法,而应该因为它“有责任”管理这些数据和行为。

2. 对象的内存真相:不是“变量集合”,而是“状态快照”

用户的内存图展示了理论模型,实际开发中我更关注对象的内存开销生命周期

java

// 分析一下这个简单对象的内存占用
public class User {
    private String username;  // 引用:4或8字节,实际字符串在堆中
    private int age;          // 4字节
    private boolean active;   // 1字节(但对齐后可能是4字节)
    private Date createdAt;   // 引用:4或8字节
    
    // 对象头:12-16字节(32/64位系统不同)
    // 对齐填充:为了8字节对齐可能有额外填充
}

// 创建100万个User对象
List<User> users = new ArrayList<>(1_000_000);
for (int i = 0; i < 1_000_000; i++) {
    users.add(new User("user" + i, 25, true, new Date()));
}

// 实际内存占用可能达到:
// 对象本身:约 40字节 × 100万 = 40MB
// 字符串:约 20字节 × 100万 = 20MB  
// Date对象:约 24字节 × 100万 = 24MB
// 总计:约 84MB!还不包括ArrayList的内部数组

这就是为什么我的第一个版本会内存溢出——我创建了太多“臃肿”的对象。优化后:

java

// 优化1:使用基本类型数组存储批量数据(查询场景)
public class UserBatch {
    private String[] usernames;
    private int[] ages;
    private boolean[] actives;
    private long[] createdAtTimestamps;  // 用long代替Date
    
    // 内存减少约60%
}

// 优化2:对象池(频繁创建销毁场景)
public class UserPool {
    private static final Queue<User> pool = new ConcurrentLinkedQueue<>();
    
    public static User borrow() {
        User user = pool.poll();
        return user != null ? user : new User();
    }
    
    public static void returnToPool(User user) {
        user.clear();  // 重置状态
        pool.offer(user);
    }
}

// 优化3:不可变对象(线程安全,可缓存)
public final class ImmutableUser {
    private final String username;
    private final int age;
    
    public ImmutableUser(String username, int age) {
        this.username = username;
        this.age = age;
    }
    
    // 只有getter,没有setter
    // 可以安全地在多线程间共享,也可以缓存
}

二、封装:从“语法要求”到“设计哲学”

1. 封装的三个层次

大多数教程只讲“private + getter/setter”,但这只是语法层面的封装。真正的封装有三个层次:

image-20260201201012672

java

// 层次1:语法封装(最基础)
public class BankAccount {
    private double balance;
    
    public double getBalance() { return balance; }
    public void setBalance(double balance) { this.balance = balance; }
    // ❌ 问题:外部还是可以直接修改余额,没有保护
}

// 层次2:行为封装(添加业务规则)
public class BankAccount {
    private double balance;
    
    public double getBalance() { return balance; }
    
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            logTransaction("存款", amount);
        } else {
            throw new IllegalArgumentException("存款金额必须为正");
        }
    }
    
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            logTransaction("取款", amount);
        } else {
            throw new IllegalArgumentException("余额不足或金额无效");
        }
    }
    // ✅ 改进:控制了修改方式,添加了业务规则
}

// 层次3:概念封装(隐藏实现细节)
public interface BankAccount {
    double getBalance();
    void deposit(double amount);
    void withdraw(double amount);
    TransactionHistory getTransactionHistory();
}

public class SavingsAccount implements BankAccount {
    private final AccountCore core;  // 核心逻辑委托给其他类
    private final InterestCalculator interestCalculator;
    
    @Override
    public void deposit(double amount) {
        core.validateDeposit(amount);
        double actualAmount = interestCalculator.calculateDepositWithInterest(amount);
        core.recordDeposit(actualAmount);
        notifyAccountChange();
    }
    // ✅ 高级:完全隐藏实现,可以随时替换利息计算算法
}

2. 封装的“边界思维”

我的学生管理系统第二次重构失败,就是因为没有理解封装的边界

java

// ❌ 错误的封装:泄露内部细节
public class Student {
    private List<Course> courses;
    
    // 错误:返回了内部可变集合
    public List<Course> getCourses() {
        return courses;  // 外部可以修改这个list!
    }
}

// 调用方可能这样:
List<Course> courses = student.getCourses();
courses.clear();  // 清空了学生的课程!灾难!

// ✅ 正确的封装:保护内部状态
public class Student {
    private List<Course> courses;
    
    // 返回不可变视图
    public List<Course> getCourses() {
        return Collections.unmodifiableList(courses);
    }
    
    // 或者返回副本
    public List<Course> getCoursesCopy() {
        return new ArrayList<>(courses);
    }
    
    // 更好的方式:不暴露集合,暴露业务方法
    public void addCourse(Course course) { ... }
    public void removeCourse(Course course) { ... }
    public boolean hasCourse(Course course) { ... }
    public int getCourseCount() { ... }
}

3. 何时应该打破封装?

封装不是绝对的。在某些性能关键路径,我们会有意打破封装:

java

// 场景:游戏开发中的向量运算(每秒数百万次调用)
public class Vector3 {
    // 通常应该private,但这里为了性能public
    public float x, y, z;
    
    public Vector3(float x, float y, float z) {
        this.x = x; this.y = y; this.z = z;
    }
    
    // 允许直接访问字段,避免方法调用开销
}

// 使用
Vector3 a = new Vector3(1, 2, 3);
Vector3 b = new Vector3(4, 5, 6);

// 直接操作字段(比通过getter/setter快得多)
float dotProduct = a.x * b.x + a.y * b.y + a.z * b.z;

规则:只有当你完全控制所有使用代码,并且性能收益确实显著时,才考虑打破封装。

三、构造器:从“初始化工具”到“对象创建策略”

image-20260201201526961

1. 构造器的进阶用法

用户提到了基础构造器,但在实际项目中,我们使用更高级的模式:

java

// 模式1:静态工厂方法(比构造器更灵活)
public class Connection {
    private final String url;
    private final int timeout;
    
    private Connection(String url, int timeout) {
        this.url = url;
        this.timeout = timeout;
    }
    
    // 静态工厂方法:可以起有意义的名字
    public static Connection createWithTimeout(String url, int timeout) {
        validateUrl(url);
        validateTimeout(timeout);
        return new Connection(url, timeout);
    }
    
    // 静态工厂方法:可以返回子类
    public static Connection createSecure(String url) {
        if (url.startsWith("https://")) {
            return new SecureConnection(url);
        }
        return new Connection(url, DEFAULT_TIMEOUT);
    }
    
    // 静态工厂方法:可以缓存对象
    private static final Map<String, Connection> cache = new ConcurrentHashMap<>();
    
    public static Connection getCached(String url) {
        return cache.computeIfAbsent(url, 
            key -> new Connection(key, DEFAULT_TIMEOUT));
    }
}

// 模式2:建造者模式(处理复杂对象)
public class Computer {
    private final String cpu;
    private final String memory;
    private final String storage;
    private final String graphicsCard;
    
    private Computer(Builder builder) {
        this.cpu = builder.cpu;
        this.memory = builder.memory;
        this.storage = builder.storage;
        this.graphicsCard = builder.graphicsCard;
    }
    
    public static class Builder {
        private String cpu;
        private String memory;
        private String storage;
        private String graphicsCard = "集成显卡";  // 默认值
        
        public Builder setCpu(String cpu) {
            this.cpu = cpu;
            return this;
        }
        
        public Builder setMemory(String memory) {
            this.memory = memory;
            return this;
        }
        
        // ... 其他setter
        
        public Computer build() {
            validate();
            return new Computer(this);
        }
        
        private void validate() {
            if (cpu == null) throw new IllegalStateException("CPU必须设置");
            if (memory == null) throw new IllegalStateException("内存必须设置");
        }
    }
}

// 使用:链式调用,清晰易读
Computer gamingPC = new Computer.Builder()
    .setCpu("Intel i9")
    .setMemory("32GB")
    .setStorage("1TB SSD")
    .setGraphicsCard("RTX 3080")
    .build();

2. 构造器中的异常处理

这是我早期常犯的错误:

java

// ❌ 危险:构造器中可能抛出异常,对象处于不一致状态
public class FileProcessor {
    private final File file;
    private final BufferedReader reader;
    
    public FileProcessor(String filePath) throws IOException {
        this.file = new File(filePath);
        this.reader = new BufferedReader(new FileReader(file));
        // 如果这里抛出异常,file已经被赋值,但reader为null
    }
}

// ✅ 安全:使用工厂方法或延迟初始化
public class FileProcessor {
    private final File file;
    private BufferedReader reader;
    
    private FileProcessor(String filePath) {
        this.file = new File(filePath);
    }
    
    public static FileProcessor create(String filePath) throws IOException {
        FileProcessor processor = new FileProcessor(filePath);
        processor.initializeReader();
        return processor;
    }
    
    private void initializeReader() throws IOException {
        this.reader = new BufferedReader(new FileReader(file));
    }
}

3. 不可变对象的构造优化

对于不可变对象,构造器有特殊优化:

java

public final class RGBColor {
    private final int red, green, blue;
    
    // 私有构造器,通过工厂方法控制实例
    private RGBColor(int red, int green, int blue) {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }
    
    // 缓存常用颜色,避免重复创建
    private static final Map<String, RGBColor> CACHE = new HashMap<>();
    
    static {
        CACHE.put("RED", new RGBColor(255, 0, 0));
        CACHE.put("GREEN", new RGBColor(0, 255, 0));
        CACHE.put("BLUE", new RGBColor(0, 0, 255));
    }
    
    public static RGBColor of(String name) {
        RGBColor color = CACHE.get(name.toUpperCase());
        if (color == null) {
            throw new IllegalArgumentException("未知颜色: " + name);
        }
        return color;
    }
    
    public static RGBColor of(int red, int green, int blue) {
        // 验证参数
        if (red < 0 || red > 255) throw new IllegalArgumentException("红色分量无效");
        if (green < 0 || green > 255) throw new IllegalArgumentException("绿色分量无效");
        if (blue < 0 || blue > 255) throw new IllegalArgumentException("蓝色分量无效");
        
        // 可以添加缓存逻辑:如果颜色已存在,返回缓存实例
        String key = red + "," + green + "," + blue;
        return CACHE.computeIfAbsent(key, 
            k -> new RGBColor(red, green, blue));
    }
}

四、this关键字:从“语法糖”到“表达力工具”

image-20260201202039991

1. this的高级用法

除了区分变量名,this在高级用法中很强大:

java

// 用法1:实现流畅接口(Fluent Interface)
public class QueryBuilder {
    private String select;
    private String from;
    private String where;
    
    public QueryBuilder select(String columns) {
        this.select = "SELECT " + columns;
        return this;
    }
    
    public QueryBuilder from(String table) {
        this.from = "FROM " + table;
        return this;
    }
    
    public QueryBuilder where(String condition) {
        this.where = "WHERE " + condition;
        return this;
    }
    
    public String build() {
        return select + " " + from + " " + where;
    }
}

// 使用:像自然语言一样流畅
String sql = new QueryBuilder()
    .select("id, name, age")
    .from("users")
    .where("age > 18")
    .build();

// 用法2:在内部类中访问外部类实例
public class Outer {
    private String name = "外部类";
    
    public class Inner {
        private String name = "内部类";
        
        public void printNames() {
            System.out.println("内部类name: " + this.name);
            System.out.println("外部类name: " + Outer.this.name);  // 明确指定
        }
    }
}

// 用法3:构造器委托链
public class Person {
    private String name;
    private int age;
    private String email;
    
    // 主要构造器(包含所有参数)
    public Person(String name, int age, String email) {
        this.name = Objects.requireNonNull(name, "姓名不能为空");
        this.age = age;
        this.email = email;
    }
    
    // 委托给主要构造器,提供默认值
    public Person(String name) {
        this(name, 0, null);  // 年龄默认为0,邮箱默认为null
    }
    
    // 另一个委托构造器
    public Person(String name, int age) {
        this(name, age, null);
    }
}

2. this在Lambda表达式中的陷阱

Java 8引入Lambda后,this的含义有了微妙变化:

java

public class ThisInLambda {
    private String value = "外部类";
    
    public void test() {
        // 传统匿名内部类
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this.getClass());  // 匿名内部类
                // System.out.println(this.value);  // 编译错误,无法访问外部类
            }
        };
        
        // Lambda表达式
        Runnable r2 = () -> {
            System.out.println(this.getClass());  // 外部类ThisInLambda
            System.out.println(this.value);       // 可以访问:"外部类"
        };
        
        r1.run();
        r2.run();
    }
}

关键区别:Lambda中的this指向包含它的外部类实例,而匿名内部类中的this指向内部类实例本身。

五、综合案例:从“作业实现”到“生产级设计”

image-20260201202413725

用户的学生管理系统案例是教学版,实际生产环境要考虑更多:

java

// 生产级的学生实体类
public class Student implements Serializable {
    private final StudentId id;  // 值对象,而非String
    private final PersonName name;  // 值对象,包含校验逻辑
    private final Age age;  // 值对象,封装年龄规则
    private final AcademicRecord academicRecord;
    private final EnrollmentStatus status;
    private final AuditInfo auditInfo;  // 审计信息:创建时间、修改时间等
    
    // 私有构造器,强制使用建造者
    private Student(Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.age = builder.age;
        this.academicRecord = builder.academicRecord;
        this.status = builder.status;
        this.auditInfo = new AuditInfo(builder.createdBy);
    }
    
    // 值对象:强化领域概念
    public static class StudentId {
        private final String value;
        
        public StudentId(String value) {
            if (value == null || !value.matches("^S\\d{8}$")) {
                throw new IllegalArgumentException("学号格式不正确");
            }
            this.value = value;
        }
        
        public String getValue() { return value; }
        
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof StudentId)) return false;
            return value.equals(((StudentId) o).value);
        }
        
        @Override
        public int hashCode() { return value.hashCode(); }
    }
    
    // 建造者模式
    public static class Builder {
        private StudentId id;
        private PersonName name;
        private Age age;
        private AcademicRecord academicRecord = new AcademicRecord();
        private EnrollmentStatus status = EnrollmentStatus.ACTIVE;
        private String createdBy;
        
        public Builder id(String id) {
            this.id = new StudentId(id);
            return this;
        }
        
        public Builder name(String firstName, String lastName) {
            this.name = new PersonName(firstName, lastName);
            return this;
        }
        
        public Builder age(int age) {
            this.age = new Age(age);
            return this;
        }
        
        public Builder createdBy(String username) {
            this.createdBy = username;
            return this;
        }
        
        public Student build() {
            validate();
            return new Student(this);
        }
        
        private void validate() {
            if (id == null) throw new IllegalStateException("学号必须设置");
            if (name == null) throw new IllegalStateException("姓名必须设置");
            if (age == null) throw new IllegalStateException("年龄必须设置");
            if (createdBy == null) throw new IllegalStateException("创建者必须设置");
        }
    }
    
    // 业务方法:体现对象责任
    public Student enrollInCourse(Course course, String enrolledBy) {
        if (!status.canEnroll()) {
            throw new IllegalStateException("学生状态不允许选课");
        }
        
        academicRecord.enroll(course);
        auditInfo.recordEnrollment(enrolledBy);
        
        // 返回新对象(不可变对象模式)
        return this;  // 或者返回一个新Student对象
    }
    
    // 领域事件
    public StudentRegisteredEvent toRegisteredEvent() {
        return new StudentRegisteredEvent(
            id.getValue(),
            name.getFullName(),
            age.getValue(),
            auditInfo.getCreatedAt()
        );
    }
}

// 使用示例
public class StudentService {
    public Student registerNewStudent(RegistrationRequest request, String operator) {
        Student student = new Student.Builder()
            .id(generateStudentId())
            .name(request.getFirstName(), request.getLastName())
            .age(request.getAge())
            .createdBy(operator)
            .build();
        
        // 发布领域事件
        eventPublisher.publish(student.toRegisteredEvent());
        
        return student;
    }
}

六、面向对象设计原则(初探)

虽然这是Day5的内容,但我想提前分享这些影响我整个职业生涯的原则:

1. SOLID原则初识

  • S(单一职责):一个类只做一件事
  • O(开闭原则):对扩展开放,对修改关闭
  • L(里氏替换):子类可以替换父类
  • I(接口隔离):客户端不应该依赖不需要的接口
  • D(依赖倒置):依赖抽象,而非具体

2. 四个简单问题判断设计好坏

在写每个类时问自己:

  1. 这个类的名字是否清晰表达了它的职责?
  2. 这个类是否只有一个修改的原因
  3. 这个类的大小是否让它在屏幕上完整可见?
  4. 其他开发人员是否能不用看实现就知道如何使用这个类?

结语:面向对象是一场思维革命

两年前那个让我重构三次的学生管理系统,今天看来是我职业生涯的转折点。它让我明白:面向对象不是语法规则,而是思考复杂系统的方式

从“面向过程”到“面向对象”的转变,不是学习class关键字和new操作符,而是学习:

  • 如何识别现实世界中的概念和关系
  • 如何分配职责给合适的对象
  • 如何设计对象之间的协作契约
  • 如何管理对象的状态和生命周期

好的面向对象设计像一部运转良好的机器——每个零件(对象)都有明确的职责,通过清晰的接口(方法)协作,共同完成复杂任务。

Day5的内容看似只是开始,但你已经站在了Java编程真正的大门。今天你建立的面向对象思维,将影响你未来写的每一行代码。

明天,当你学习继承和多态时,你会看到这些基础如何构建更强大的抽象。但那是明天的故事了。

今天,好好思考你设计的每一个类——它们不只是代码,更是你对问题领域的理解和表达。

知识点测试

读完文章了?来测试一下你对知识点的掌握程度吧!

评论区

使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。

如果评论系统无法加载,请确保:

  • 您的网络可以访问 GitHub
  • giscus GitHub App 已安装到仓库
  • 仓库已启用 Discussions 功能